﻿namespace Jsl.FxCop
{
    using System;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Xml;
    using Microsoft.FxCop.Sdk;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class BuildDgmlRepresentation
    {
        [TestMethod]
        public void BuildClassDgml()
        {
            Assert.Inconclusive("Only run interactively.");

            const string ClassCode = @"
using System.ComponentModel;
using System.Windows.Forms;

public class Class : Form
{
    private IContainer components;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (components != null)
            {
                components.Dispose();
            }

            try
            {
                this.Text = ""xyz"";
            }
            catch (System.Exception ex)
            {
                ExceptionBox.Show(ex);
            }
        }
        base.Dispose(disposing);
    }
}

public class ExceptionBox
{
    public static void Show(System.Exception ex)
    {
    }
}";

            foreach (var debugMode in new bool[] { false, true })
            {
                var method = CompiledCode.GetMethodFromClass(ClassCode, "Dispose", debugMode);
                WriteDgml(method, debugMode);
            }
        }

        public void Method()
        {
            string[] strings = null;
            foreach (object value in strings)
            {
            }
        }

        [TestMethod]
        public void BuildMethodDgml()
        {
            Assert.Inconclusive("Only run interactively.");

            const string MethodCode = @"
public object Method()
{
    object x = ""abc"";

    return x;
}";

            foreach (var debugMode in new bool[] { false, true })
            {
                var method = CompiledCode.GetMethod(MethodCode, debugMode);
                WriteDgml(method, debugMode);
            }
        }

        [TestMethod]
        public void BuildSpecificMethodDgml()
        {
            Assert.Inconclusive("Only run interactively.");

            var assembly = AssemblyNode.GetAssembly(@"PATH");
            var type = assembly.GetType(Identifier.For("NAMESPACE"), Identifier.For("TYPE"));
            var method = type.GetMethod("METHOD");

            WriteDgml(method, false);
        }

        #region Private Methods
        private static string BuildCastclassExpressionName(BinaryExpression castclassExpression)
        {
            return "("
                + (castclassExpression.Operand2 as Literal)
                    .IfNotNull(literal => literal.Value as TypeNode)
                    .IfNotNull(classNode => classNode.Name.Name)
                    .IfNull("UNKNOWN")
                + ")"
                + BuildExpressionName(castclassExpression.Operand1);
        }

        private static string BuildIsinstExpressionName(BinaryExpression isinstExpression)
        {
            return BuildExpressionName(isinstExpression.Operand1)
                + " as "
                + (isinstExpression.Operand2 as Literal)
                    .IfNotNull(literal => literal.Value as TypeNode)
                    .IfNotNull(classNode => classNode.Name.Name)
                    .IfNull("UNKNOWN");
        }

        private static string BuildExpressionName(Expression expression)
        {
            switch (expression.NodeType)
            {
                case NodeType.Add:
                    return BuildSimpleBinaryExpressionName((BinaryExpression)expression, " + ");

                case NodeType.AddressDereference:
                    return "ValueAt(" + BuildExpressionName(((AddressDereference)expression).Address) + ")";

                case NodeType.Call:
                case NodeType.Callvirt:
                    return BuildMethodCallExpressionName((MethodCall)expression);

                case NodeType.Castclass:
                    return BuildCastclassExpressionName((BinaryExpression)expression);

                case NodeType.Ceq:
                case NodeType.Eq:
                    return BuildSimpleBinaryExpressionName((BinaryExpression)expression, " == ");

                case NodeType.Clt:
                case NodeType.Clt_Un:
                    return BuildSimpleBinaryExpressionName((BinaryExpression)expression, " < ");

                case NodeType.Construct:
                    return BuildConstructExpressionName((Construct)expression);

                case NodeType.Conv_I4:
                    return "Int32(" + BuildExpressionName(((UnaryExpression)expression).Operand) + ")";

                case NodeType.Indexer:
                    return "Indexer(" + BuildExpressionName(((Indexer)expression).Object) + ")";

                case NodeType.Isinst:
                    return BuildIsinstExpressionName((BinaryExpression)expression);

                case NodeType.Ldlen:
                    return "Ldlen(" + BuildExpressionName(((UnaryExpression)expression).Operand) + ")";

                case NodeType.Literal:
                    return BuildLiteralExpressionName((Literal)expression);

                case NodeType.Local:
                case NodeType.StackVariable:
                    return BuildLocalExpressionName((Local)expression);

                case NodeType.LogicalNot:
                    return BuildUnaryExpressionName((UnaryExpression)expression, "!");

                case NodeType.Lt:
                    return BuildSimpleBinaryExpressionName((BinaryExpression)expression, " < ");

                case NodeType.MemberBinding:
                    return BuildMemberBindingExpressionName((MemberBinding)expression);

                case NodeType.Ne:
                    return BuildSimpleBinaryExpressionName((BinaryExpression)expression, " != ");

                case NodeType.Parameter:
                    return BuildParameterExpressionName((Parameter)expression);

                case NodeType.This:
                    return "this";

                case NodeType.Pop:
                    return (expression as UnaryExpression)
                        .IfNotNull(unary => "pop " + BuildExpressionName(unary.Operand))
                        .IfNull("pop");

                default:
                    return "? " + expression.GetType().Name + " ?";
            }
        }

        private static string BuildSimpleBinaryExpressionName(BinaryExpression binaryExpression, string @operator)
        {
            return BuildExpressionName(binaryExpression.Operand1)
                + @operator
                + BuildExpressionName(binaryExpression.Operand2);
        }

        private static string BuildConstructExpressionName(Construct construct)
        {
            var operandList =
                from operand in construct.Operands
                select BuildExpressionName(operand);

            var boundMember = (MemberBinding)construct.Constructor;

            return "new "
                + boundMember.BoundMember.DeclaringType.Name.Name
                + "("
                + string.Join(", ", operandList)
                + ")";
        }

        private static string BuildLiteralExpressionName(Literal literal)
        {
            if (literal.Value == null)
            {
                return "null";
            }
            else if (literal.Value is string)
            {
                return "\"" + ((string)literal.Value).Replace("\"", "\\\"") + "\"";
            }
            else
            {
                return literal.Value.ToString();
            }
        }

        private static string BuildLocalExpressionName(Local local)
        {
            return local.Name.Name;
        }

        private static string BuildMemberBindingExpressionName(MemberBinding memberBinding)
        {
            if (memberBinding.TargetObject == null)
            {
                return memberBinding.BoundMember.DeclaringType.Name.Name
                    + "."
                    + memberBinding.BoundMember.Name.Name;
            }
            else
            {
                return BuildExpressionName(memberBinding.TargetObject)
                    + "."
                    + memberBinding.BoundMember.Name.Name;
            }
        }

        private static string BuildMethodCallExpressionName(MethodCall methodCall)
        {
            var operandNames = methodCall.Operands
                .Select(operand => BuildExpressionName(operand));

            return BuildExpressionName(methodCall.Callee)
                + "("
                + string.Join(", ", operandNames)
                + ")";
        }

        private static string BuildParameterExpressionName(Parameter parameter)
        {
            return parameter.Name.Name;
        }

        private static string BuildUnaryExpressionName(UnaryExpression unaryExpression, string operatorText)
        {
            return operatorText + BuildExpressionName(unaryExpression.Operand);
        }

        private static string BuildStatementName(Statement statement)
        {
            switch (statement.NodeType)
            {
                case NodeType.AssignmentStatement:
                    return BuildStatementName((AssignmentStatement)statement);

                case NodeType.Block:
                    return "Block";

                case NodeType.Branch:
                    return BuildStatementName((Branch)statement);

                case NodeType.Catch:
                    return BuildStatementName((CatchNode)statement);

                case NodeType.EndFinally:
                    return "end-finally";

                case NodeType.ExpressionStatement:
                    return BuildStatementName((ExpressionStatement)statement);

                case NodeType.Finally:
                    return "finally";

                case NodeType.Nop:
                    return "nop";

                case NodeType.Return:
                    return BuildStatementName((ReturnNode)statement);

                case NodeType.Try:
                    return "try";

                default:
                    return "? " + statement.GetType().Name + " ?";
            }
        }

        private static string BuildStatementName(AssignmentStatement assignmentStatement)
        {
            return BuildExpressionName(assignmentStatement.Target)
                + " = "
                + BuildExpressionName(assignmentStatement.Source);
        }

        private static string BuildStatementName(Branch branch)
        {
            if (branch.Condition == null)
            {
                if (branch.LeavesExceptionBlock)
                {
                    return "leave exception";
                }
                else
                {
                    return "goto";
                }
            }
            else
            {
                return "if (" + BuildExpressionName(branch.Condition) + ")";
            }
        }

        private static string BuildStatementName(CatchNode catchNode)
        {
            return "catch ("
                + catchNode.Type.Name.Name
                + " "
                + BuildExpressionName(catchNode.Variable)
                + ")";
        }

        private static string BuildStatementName(ExpressionStatement expressionStatement)
        {
            return BuildExpressionName(expressionStatement.Expression);
        }

        private static string BuildStatementName(ReturnNode returnStatement)
        {
            if (returnStatement.Expression == null)
            {
                return "return";
            }
            else
            {
                return "return " + BuildExpressionName(returnStatement.Expression);
            }
        }

        private static void WriteDgml(Method method, bool debugMode)
        {
            // Create the DGML file on the Desktop.
            var desktopDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
            var outputPath = Path.Combine(desktopDirectory, debugMode ? "BuildDgml_Debug.dgml" : "BuildDgml.dgml");
            using (var writer = new XmlTextWriter(outputPath, Encoding.UTF8))
            {
                var flow = new StatementFlow(method).Prune();

                writer.WriteStartDocument(true);
                writer.WriteStartElement("DirectedGraph", "http://schemas.microsoft.com/vs/2009/dgml");

                WriteDgmlNodes(writer, flow);

                WriteDgmlLinks(writer, flow);

                writer.WriteEndElement();
                writer.WriteEndDocument();
            }
        }

        private static void WriteDgmlLinks(XmlTextWriter writer, StatementFlow flow)
        {
            writer.WriteStartElement("Links");

            foreach (var statement in flow.Statements)
            {
                var followingStatements = flow.FollowingStatements(statement);
                var firstFollowingStatement = true;
                foreach (var followingStatement in followingStatements)
                {
                    writer.WriteStartElement("Link");
                    writer.WriteAttributeString("Source", statement.UniqueKey.ToString());
                    writer.WriteAttributeString("Target", followingStatement.UniqueKey.ToString());

                    var branch = statement as Branch;
                    if ((branch != null) && (followingStatements.Count() > 1))
                    {
                        writer.WriteAttributeString("Label", firstFollowingStatement ? "True" : "False");
                    }

                    writer.WriteEndElement();

                    firstFollowingStatement = false;
                }
            }

            writer.WriteEndElement();
        }

        private static void WriteDgmlNodes(XmlTextWriter writer, StatementFlow flow)
        {
            writer.WriteStartElement("Nodes");

            foreach (var statement in flow.Statements)
            {
                writer.WriteStartElement("Node");
                writer.WriteAttributeString("Id", statement.UniqueKey.ToString());
                writer.WriteAttributeString("Label", BuildStatementName(statement));
                writer.WriteEndElement();
            }

            writer.WriteEndElement();
        }
        #endregion Private Methods
    }
}